<?php
error_reporting(~E_NOTICE);
define('TXTDB',true);
define('TXTDB_DEBUG',true);
define('TXTDB_SEPARATOR','|');
define('TXTDB_VERSION','0.4.0.4');
/**
 * @abstract Text DataBase API
 * @author episome <eipsome(at)gmail.com>
 * @author milkliker <milkliker(at)gmail.com>
 * @link http://www.3ants.org
 * @copyright The Three Ants
 * @version  0.4.0.4
 */
class txtdb{
	var $auto_id_name = 'id';
	var $exten = '.tbl.php';
	var $db = array();
	var $tbls = array();


	/**
	 * @return void
	 * @param string $path[optional]
	 * @param string $cache_dir[optional]
	 * @desc constructor.
	 */
	function txtdb($path = null, $cache_dir = null){
		if(!is_null($path)){
			$this->connect($path, $cache_dir);
		}
	}
	public function __construct($path = null, $cache_dir = null){
		$this->txtdb($path, $cache_dir);
	}
	/**
	 * @return boolean
	 * @param string $path
	 * @param string $cache_dir[optional]
	 * @desc connect to txtdb.
	 */
	function connect($path, $cache_dir = null){
		if(substr($path, -1) != "/"){
			$path .= "/";
		}
		if(!is_writable($path)){
			$this->_halt("Setting datebase directory ($path) is not a valid path or not writeable, try chmod 0777 if 0666 does not work.");
		}
		unset($this->db);
		unset($this->tbls);
		$this->db['connect'] = true;
		$this->db['path'] = $path;
		$this->db['opens'] = 0;
		$this->db['denyMsg'] = '<?php die(\'txtdb:' . TXTDB_VERSION . '\');?' . '>';
		if(is_null($cache_dir)){
			return true;
		}
		if(substr($cache_dir, -1) != "/"){
			$cache_dir .= "/";
		}
		if(!is_writable($cache_dir)){
			$this->_halt("Setting cache directory ($cache_dir) is not a valid path or not writeable, try chmod 0777 if 0666 does not work.");
		}
		$this->db['cache_dir'] = $cache_dir;
		return true;
	}
	/**
	 * @desc get,search,sort record(s)
	 * @param mixed $expression
	 * @param string $limit
	 * @param string $table
	 * @param boolean $cache
	 * @param string $sortby
	 * @param asc|desc $flag
	 * @return array
	 */
	function get($expression, $limit = null, $table = null, $cache = false, $sortby = null, $flags = 'asc'){
		if($cache){
			$_cache_file = $this->db['cache_dir'] . 'txtdb_' . md5($table . $expression . $limit . $sortby . $flags).$this->exten;
			if($this->_get_cache_mtime($_cache_file) > $this->get_table_mtime($table)){
				return unserialize(substr($this->_read($_cache_file),strlen($this->db['denyMsg'])));
			}
		}
		$this->_check_table($table);
		$target = array();
		if(is_null($limit)){
			$this->_make_writeable($table);
		}
		if(preg_match("/^\d+(\,\d+)*$/", (string)$expression) && $expression !== true){
			$ids = array_unique(explode(',', $expression));// "0,4,5,48,22"
			if($this->tbls[$table]['writeable']){
				sort($ids);
				$_count = count($ids);
				for($i = 0; $i < $_count && $ids[$i] < $this->tbls[$table]['rows']; $i++){
					$this->_build_values($ids[$i], $table);
					$target[$ids[$i]] = $this->_special_character_out($this->tbls[$table]['values'][$ids[$i]]);
				}
			}else{
				$end = @max($ids);
				$this->fp[$table] = @fopen($this->tbls[$table]['path'], 'rb');
				@flock($this->fp[$table], LOCK_SH);
				$this->tbls[$table]['header'] = @fgets($this->fp[$table], $this->tbls[$table]['maximum']);
				for($id = 0; $id <= $end && $id < $this->tbls[$table]['rows']; $id++){
					$this->tbls[$table]['records'][$id] = @fgets($this->fp[$table], $this->tbls[$table]['maximum']);
					if(!in_array($id, $ids)){
						continue;
					}
					$this->_build_values($id, $table);
					$target[$id] = $this->_special_character_out($this->tbls[$table]['values'][$id]);
				}
				@fclose($this->fp[$table]);
			}
		}else{
			$limit = $this->_get_limit($limit, $table);
			if(!$this-> tbls[$table]['writeable']){
				$id = 0;
				$this->fp[$table] = @fopen($this->tbls[$table]['path'], "rb");
				@flock($this->fp[$table], LOCK_SH);
				$this->tbls[$table]['header'] = @fgets($this->fp[$table], $this->tbls[$table]['maximum']);
			}else{
				$id = $limit['startPos'];
			}
			$_exp_matches = $this->_match_expression($expression);
			for($id, $num = 0; $num < $limit['num'] && $id < $this->tbls[$table]['rows']; $id++){
				if(!$this-> tbls[$table]['writeable']){
					$this->tbls[$table]['records'][$id] = @fgets($this->fp[$table], $this->tbls[$table]['maximum']);
					if($id < $limit['startPos']){
						continue;
					}
				}
				$this->_build_values($id, $table);
				$exp = $expression;
				foreach($_exp_matches as $match){
					$exp = str_replace('{' . $match . '}', $this->tbls[$table]['values'][$id][$match], $exp);
				}
				$result = false;
				@eval("\$result=($exp);");
				if($result === true || $result===1){
					$num ++;
					$target[$id] = $this->_special_character_out($this->tbls[$table]['values'][$id]);
				}
			}
			if(!$this-> tbls[$table]['writeable']){
				@fclose($this->fp[$table]);
			}
		}
		if(!is_null($sortby) && in_array($sortby, $this->tbls[$table]['fields'])){
			$target = $this->_sort($target, $sortby, $flags);
		}
		if($cache){
			$this->_write($this->db['denyMsg'].serialize($target),$_cache_file);
		}
		return $target;
	}
	/**
	 * @desc update record(s)
	 * @param array $values
	 * @param mixed $expression
	 * @param string $limit
	 * @param string $table
	 * @return boolean
	 */
	function set($values, $expression, $limit = null, $table = null){
		if(!is_array($values)){
			return false;
		}
		$this->_check_table($table, true);
		unset($values[$this->auto_id_name]);
		if((preg_match("/^\d+(\,\d+)*$/", (string)$expression)) && $expression !== true){
			$ids = array_unique(explode(',', $expression));# "1,435,76"
			foreach ($ids as $id){
				if($id >= $this->tbls[$table]['rows']){
					continue;
				}
				$this->_build_values($id, $table);
				foreach($values as $feild => $value){
					if(in_array($feild, $this->tbls[$table]['fields'])){
						$this->tbls[$table]['values'][$id][$feild] = $this->_special_character_in($value);
					}
				}
				$this->tbls[$table]['records'][$id] = implode(TXTDB_SEPARATOR, $this->tbls[$table]['values'][$id]);
				$this->tbls[$table]['maximum'] = max($this->tbls[$table]['maximum'], strlen($this->tbls[$table]['records'][$id]) + 10);
			}
		}else{
			$limit = $this->_get_limit($limit, $table);
			$_exp_matches = $this->_match_expression($expression);
			for($id = $limit['startPos'], $num = 0; $num < $limit['num'] && $id < $this->tbls[$table]['rows']; $id++){
				$this->_build_values($id, $table);
				$exp = $expression;
				foreach($_exp_matches as $match){
					$exp = str_replace('{' . $match . '}', $this->tbls[$table]['values'][$id][$match], $exp);
				}
				$result = false;
				@eval("\$result=($exp);");
				if($result === true || $result===1){
					$num ++;
					foreach($values as $feild => $value){
						if(in_array($feild, $this->tbls[$table]['fields'])){
							$this->tbls[$table]['values'][$id][$feild] = $this->_special_character_in($value); //::?
						}
					}
					$this->tbls[$table]['records'][$id] = implode(TXTDB_SEPARATOR, $this->tbls[$table]['values'][$id]);
					$this->tbls[$table]['maximum'] = max($this->tbls[$table]['maximum'], strlen($this->tbls[$table]['records'][$id]) + 10);
				}
			}
		}
		return true;
	}
	/**
	 * @desc delete record(s)
	 * @param mixed $expression
	 * @param string $limit
	 * @param string $table
	 * return bool
	 */
	function del($expression, $limit = null, $table = null){
		$this->_check_table($table, true);
		if((preg_match("/^\d+(\,\d+)*$/", (string)$expression)) && $expression !== true){
			$ids = array_unique(explode(',', $expression)); # "1,2,5" or "3"
			foreach ($ids as $id){
				if(is_null($this->tbls[$table]['records'][$id])){
					continue;
				}
				unset($this->tbls[$table]['records'][$id]);
				unset($this->tbls[$table]['values'][$id]);
			}
		}else{
			$limit = $this->_get_limit($limit, $table);
			$_exp_matches = $this->_match_expression($expression);
			for($id = $limit['startPos'], $num = 0;$num < $limit['num'] && $id < $this->tbls[$table]['rows'];$id++){
				$this->_build_values($id, $table);
				$exp = $expression;
				foreach($_exp_matches as $match){
					$exp = str_replace('{' . $match . '}', $this->tbls[$table]['values'][$id][$match], $exp);
				}
				$result = false;
				@eval("\$result=($exp);");
				if($result === true || $result===1){
					$num ++;
					unset($this->tbls[$table]['records'][$id]);
					unset($this->tbls[$table]['values'][$id]);
				}
			}
		}
		$this->tbls[$table]['records'] = @array_values($this->tbls[$table]['records']);
		$this->tbls[$table]['values'] = @array_values($this->tbls[$table]['values']);
		$this->tbls[$table]['rows'] = count($this->tbls[$table]['records']);
		return true;
	}
	/**
	 * @desc append record
	 * @param array $values
	 * @param int $id
	 * @param string $table
	 * @return bool
	 */
	function append($values, $id = null, $table = null){
		if(!is_array($values)){
			return false;
		}
		$this->_check_table($table, true);
		if(is_null($id) || $id > $this->tbls[$table]['rows']){
			$id = $this->tbls[$table]['rows'];
		}elseif($id < 0){
			$id = 0;
		}
		foreach($this->tbls[$table]['fields'] as $field){
			if($field == $this->auto_id_name){
				$values[$field] = $this->tbls[$table]['auto_id'];
				$this->tbls[$table]['auto_id']++;
			}
			$_values[] = $values[$field];
		}
		$_new_record = @implode(TXTDB_SEPARATOR, $this->_special_character_in($_values));
		$this->tbls[$table]['maximum'] = max($this->tbls[$table]['maximum'], strlen($_new_record) + 10);
		$_header = @array_slice($this->tbls[$table]['records'], 0, $id);
		$_footer = @array_slice($this->tbls[$table]['records'], $id);
		$this->tbls[$table]['records'] = @array_merge($_header, array($_new_record), $_footer);
		$this->tbls[$table]['rows']++;
		array_splice($this->tbls[$table]['values'],$id);
		return true;
	}
	/**
	 * @desc create table
	 * @param string $table
	 * @param mixed $fields
	 * @return bool
	 */
	function create($table, $fields){
		if(!$this->db['connect']){
			$this->_halt('Unknow txtDb Directory!');
		}
		if(!$this->_check($table)){
			$this->_halt('Table name [' . $table . '] not allowed!');
		}
		if($this->exists($table)){
			$this->_halt('Table [' . $table . '] already exists');
		}
		if(is_string($fields)){
			$fields = explode(',', $fields);
		}
		$fields = array_unique($fields);
		foreach ($fields as $id => $field){
			if(trim($field) == '') unset($fields[$id]);
		}
		if(!$this->_check($fields)){
			$this->_halt('Fields name not allowed!');
		}
		$this->tbls[$table]['path'] = $this->db['path'] . $table . $this->exten;
		$this->tbls[$table]['auto_id'] = '0';
		$this->tbls[$table]['fields'] = $fields;
		$this->tbls[$table]['rows'] = '0';
		$this->tbls[$table]['header'] = $this->db['denyMsg'] . TXTDB_SEPARATOR . $this->tbls[$table]['rows'] . TXTDB_SEPARATOR . $this->tbls[$table]['auto_id'] . TXTDB_SEPARATOR . 'M*A*X*I*M*U*M' . TXTDB_SEPARATOR . @implode(TXTDB_SEPARATOR, $this->tbls[$table]['fields']);
		$this->tbls[$table]['maximum'] = strlen($this->tbls[$table]['header']);
		$this->tbls[$table]['header'] = str_replace('M*A*X*I*M*U*M', $this->tbls[$table]['maximum'], $this->tbls[$table]['header']);
		if(!$this->_write($this->tbls[$table]['header'], $this->tbls[$table]['path'])){
			$this->_halt('Can\'t write to table [' . $table . '].');
		}
		$this->tbls[$table]['open'] = true;
		$this->tbls[$table]['writeable'] = true;
		$this->tbls[$table]['cols'] = count($this->tbls[$table]['fields']);
		$this->tbls[$table]['records'] = array();
		$this->tbls[$table]['values'] = array();
		$this->db['tbl'] = $table;
		$this->db['opens']++;
		return true;
	}
	/**
	 * @desc shift record
	 * @param int $source
	 * @param int $target
	 * @param string $table
	 * @return bool
	 */
	function shift($source, $target, $table = null){
		$this->_check_table($table, true);
		if($source == $target){
			return true;
		}
		if($source < 0 || $source >= $this->tbls[$table]['rows'] || $target < 0 || $target >= $this->tbls[$table]['rows']){
			return false;
		}
		$_center = array($this->tbls[$table]['records'][$source]);
		unset($this->tbls[$table]['records'][$source]);
		$_header = @array_slice($this->tbls[$table]['records'], 0, $target);
		$_footer = @array_slice($this->tbls[$table]['records'], $target);
		$this->tbls[$table]['records'] = @array_merge($_header, $_center, $_footer);
		unset($this->tbls[$table]['values'][$source]);
		unset($this->tbls[$table]['values'][$target]);
		return true;
	}
	/**
	 * @desc save table
	 * @param string $table
	 * @return bool
	 */
	function save($table = null){
		$this->_check_table($table, false, false);
		if(!$this->tbls[$table]['open']){
			$this->_halt('Table [' . $table . '] not opened!', false);
			return false;
		}
		if(!$this->tbls[$table]['writeable']){
			$this->_halt('Table [' . $table . '] not writeable!', false);
			return false;
		}
		$this->tbls[$table]['header'] = $this->db['denyMsg'] . TXTDB_SEPARATOR . $this->tbls[$table]['rows'] . TXTDB_SEPARATOR . $this->tbls[$table]['auto_id'] . TXTDB_SEPARATOR . $this->tbls[$table]['maximum'] . TXTDB_SEPARATOR . @implode(TXTDB_SEPARATOR, $this->tbls[$table]['fields']);
		$data = $this->tbls[$table]['records'];
		array_unshift($data,$this->tbls[$table]['header']);
		if(!$this->_write(implode("\n",$data), $this->tbls[$table]['path'])){
			$this->_halt('Can\'t write to table [' . $table . '].');
		}
		return true;
	}
	/**
	 * @desc drop table
	 * @param string $table
	 * @return bool
	 */
	function drop($table = null){
		$this->_check_table($table, false, false);
		if($table == $this->db['tbl']){
			unset($this->db['tbl']);
		}
		return @unlink($this->db['path'] . $table . $this->exten);
	}
	/**
	 * @desc check table exists
	 * @param string $table 
	 * @return bool
	 */
	function exists($table){
		return file_exists($this->db['path'] . $table . $this->exten);
	}
	/**
	 * @desc get table last modify time 
	 * @param string $table 
	 * @return timestamp 
	 */
	function get_table_mtime($table = null){
		$this->_check_table($table, false, false);
		return @filemtime($this->db['path'] . $table . $this->exten);
	}
	/**
	 * @desc clear table
	 * @param string $table
	 * @return void
	 */
	function clear($table = null){
		$this->_check_table($table, true);
		$this->tbls[$table]['rows'] = 0;
		$this->tbls[$table]['records'] = array();
		$this->tbls[$table]['values'] = array();
		$this->tbls[$table]['auto_id'] = '0';
		$this->tbls[$table]['maximum'] = strlen($this->tbls[$table]['header'])+10;
	}
	/**
	 * @desc get table file size
	 * @param string $table
	 * @return float
	 */
	function get_table_size($table = null){
		$this->_check_table($table, false, false);
		return filesize($this->db['path'] . $table . $this->exten);
	}
	/**
	 * @desc get fields
	 * @param string $table
	 * @return array
	 */
	function fields($table = null){
		$this->_check_table($table);
		return $this->tbls[$table]['fields'];
	}
	/**
	 * @desc get fields number
	 * @param string $table
	 * @return int
	 */
	function cols($table = null){
		$this->_check_table($table);
		return $this->tbls[$table]['cols'];
	}
	/**
	 * @desc get records number
	 * @param string $table
	 * @return int
	 */
	function rows($table = null){
		$this->_check_table($table);
		return $this->tbls[$table]['rows'];
	}
	/**
	 * @desc get table opens number
	 * @return int
	 */
	function opens(){
		return $this->db['opens'];
	}
	/**
	 * @desc get version
	 * @return string
	 */
	function version(){
		return TXTDB_VERSION;
	}
	// sort an array by $field and order by $flag
	function _sort($source, $field, $flag){
		if(!is_array($source) || count($source) <= 1){
			return $source;
		}
		$ids = array_keys($source);
		foreach($ids as $id){
			$besort[$id] = $source[$id][$field];
		}
		strtolower($flag) == 'desc' ? arsort($besort, 0) : asort($besort, 0);
		$order = array_keys($besort);
		foreach($order as $id){
			$target[$id] = $source[$id];
		}
		return $target;
	}
	// make table writeable
	function _make_writeable($table){
		$this->tbls[$table]['records'] = explode("\n", $this->_read($this->tbls[$table]['path']));
		$this->tbls[$table]['header'] = array_shift($this->tbls[$table]['records']);
		$this->tbls[$table]['writeable'] = true;
	}
	// get $_cache_file last modify time
	function _get_cache_mtime($_cache_file){
		return file_exists($_cache_file) ? @filemtime($_cache_file) : 0;
	}
	// get an limit array eg:'34','0,34'
	function _get_limit($limit, $table){
		if(is_null($limit)){
			return array('startPos' => 0, 'num' => $this->tbls[$table]['rows']);
		}
		if(!preg_match("/^\d+(\,\d+)?$/", $limit)){
			$this->_halt('Illegal limit argument!');
		}
		if(strpos($limit, ',') > 0){
			list($startPos, $num) = explode(',', $limit);
			if($startPos > $this->tbls[$table]['rows']){
				$startPos = $this->tbls[$table]['rows'];
			}
		}else{
			$startPos = 0;
			$num = $limit;
		}
		return array('startPos' => $startPos, 'num' => $num);
	}
	// build an array by record id
	function _build_values($id, $table){
		if(is_array($this->tbls[$table]['values'][$id])){
			return $this->tbls[$table]['values'][$id];
		}
		$values = @explode(TXTDB_SEPARATOR, trim($this->tbls[$table]['records'][$id]));
		foreach($this->tbls[$table]['fields'] as $key => $field){
			$this->tbls[$table]['values'][$id][$field] = $values[$key];
		}// ARRAY_COMBINE
		return $this->tbls[$table]['values'][$id];
	}
	// read the $content from $file, return as string
	function _read($filename){
		$fp = @fopen($filename, 'rb');
		@flock($fp, LOCK_SH);
		$content = @fread($fp, filesize($filename));
		@fclose($fp);
		return $content;
	}
	// write the $content to $file, return as bool
	function _write($content, $file){
		$fp = @fopen($file, 'wb');
		@flock($fp, LOCK_EX);
		if(!@fwrite($fp, $content)){
			return false;
		}
		@fclose($fp);
		return true;
	}
	// Special character input
	function _special_character_in($w){
		return str_replace(array('\\', TXTDB_SEPARATOR, "\n", "\r"),	array('&#92', '&#' . ord(TXTDB_SEPARATOR), '&#10', '&#13'), $w);
	}
	// Special character output
	function _special_character_out($w){
		return str_replace(array('&#92', '&#13', "&#10", '&#' . ord(TXTDB_SEPARATOR)), array('\\' , "\r" , "\n" , TXTDB_SEPARATOR), $w);
	}
	// check fields name or table name
	function _check($string){
		if(is_array($string)){
			$string = implode('', $string);
		}
		return trim($string) !== '' && !strpos($string, "\n") && preg_match("/^[ \/\\_0-9a-zA-Z\x80-\xff]+$/", $string);
	}
	// check table, if open..., if writeable ...
	function _check_table(&$table, $writeable = false, $open = true){
		if(is_null($table) && isset($this->db['tbl'])){
			$table = $this->db['tbl'];
			if($writeable && !$this->tbls[$table]['writeable']){
				$this->_make_writeable($table);
			}
		}else{
			if(!$this->db['connect']){
				$this->_halt('Unknow txtDb Directory!');
			}
			if(!$this->exists($table)){
				$this->_halt('Table [' . $table . '] not exists');
			}
			if($this->tbls[$table]['open']){
				if($writeable && !$this->tbls[$table]['writeable']){
					$this->_make_writeable($table);
				}
			}elseif($open){
				$this->tbls[$table]['path'] = $this->db['path'] . $table . $this->exten;
				if($writeable){
					$this->tbls[$table]['records'] = explode("\n", $this->_read($this->tbls[$table]['path']));
					$this->tbls[$table]['header'] = array_shift($this->tbls[$table]['records']); # cross out table header
				}else{
					$this->fp[$table] = @fopen($this->tbls[$table]['path'], 'rb');
					@flock($this->fp[$table], LOCK_SH);
					$this->tbls[$table]['header'] = @fgets($this->fp[$table], 4096);
					@fclose($this->fp[$table]);
				}
				$this->tbls[$table]['fields'] = @explode(TXTDB_SEPARATOR, trim($this->tbls[$table]['header']));
				array_shift($this->tbls[$table]['fields']);
				$this->tbls[$table]['writeable'] = $writeable;
				$this->tbls[$table]['rows'] = array_shift($this->tbls[$table]['fields']);
				$this->tbls[$table]['auto_id'] = array_shift($this->tbls[$table]['fields']);
				$this->tbls[$table]['maximum'] = array_shift($this->tbls[$table]['fields']);
				$this->tbls[$table]['cols'] = count($this->tbls[$table]['fields']);
				$this->tbls[$table]['values'] = array();
				$this->tbls[$table]['open'] = true;
				$this->db['opens']++;
			}
			$this->db['tbl'] = $table;
		}
	}
	// match express. exp: "{name}=='episome' and {age}==18" to array(name,age)
	function _match_expression(&$expression){
		if($expression===true || $expression===1) return array();
		$expression = $this->_special_character_in($expression);
		$_exp_matches = array();
		preg_match_all('#\{(.*?)\}#is', $expression, $_exp_matches);
		return array_unique($_exp_matches[1]);
	}
	// halt error message.
	function _halt($message, $died = true){
		!TXTDB_DEBUG || print '<script>alert("txtdb warning:\n\n'.$message.'!")</script>';
		!$died || die();
	}
}